/*
 * Created on May 8, 2004
 */
package dk.aiszone.lang.autosource.generatorobjects;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import dk.aiszone.lang.autosource.generatorobjects.helpers.ImportsList;
import dk.aiszone.lang.autosource.generatorobjects.interfaces.DeclarableObject;

/**
 * Representation of the contents of a class = the body of the class<br>
 * This object contains the <code>Methods</code> and inner classes of the generated <code>Class</code>
 * 
 * @author <a href="mailto:ais@mail.dk">Ais</a>
 * @since 1.0
 * @version 1.0
 */
public class ClassBody extends AbstractBody<Method> implements Cloneable {

    /** <code>Set</code> if <code>Classes</code> implementet by the <code>ClassBody</code> */
    protected final Set<Class> _implements;

    /** <code>Set</code> of constructors supported by the <code>ClassBody</code> (<code>Set&lt;Method&gt;</code>) */
    protected Set<Method> constructors;

    /** <code>Set</code> of <code>Variables</code> within the <code>ClassBody</code> (<code>Set&lt;Variable&gt;</code>). */
    protected Set<Variable> variables;

    private boolean serialVersionUIDAdded = false;

    private List<StaticArea> staticAreas = new ArrayList<StaticArea>();

    /**
     * Default constructor of the <code>ClassBody</code>.
     */
    public ClassBody() {
        this(null, null);
    }

    /**
     * Constructor for the <code>ClassBody</code>
     * 
     * @param declaration
     *            The <code>Declaration</code> of the <code>ClassBody</code>
     * @param name
     *            The name of the <code>ClassBody</code>.
     */
    public ClassBody(Declaration declaration, String name) {
        super(declaration, name);

        this.variables = new TreeSet<Variable>();
        this.constructors = new TreeSet<Method>();
        this._implements = new TreeSet<Class>(new ClassNameComparator());
    }

    /**
     * Add a constructor to the <code>ClassBody</code>.
     * 
     * @param constructor
     *            The constructor to be added.<br>
     *            Must be a <code>Method</code> marked as a constructor.
     * @throws IllegalArgumentException
     *             Throw new the supplied constructor is <code>null</code><br>
     *             or not marked as a constructor.
     */
    public void addConstructor(Method constructor) throws IllegalArgumentException {
        assert constructor.getReturnType() != null && Class.CONSTRUCTOR.equals(constructor.getReturnType()) : "Added Method was not marked as a constructor";

        constructor.setName(getName());

        constructors.add(constructor);
    }

    /**
     * Add a <code>Class</code> to the list of interfaces implementet by body.<br>
     * 
     * @param _implements
     */
    public void addImplements(Class _implements) {
        if (_implements.requiresImport()) {
            addNeededImports(_implements.toImports());
        }
        this._implements.add(_implements);
    }

    /**
     * Adds a <code>Variable</code> to the <code>ClassBody</code> which, when written, will generate the serialVersionUID of the class.
     */
    public void addSerialVersionUIDVariable() {
        if (serialVersionUIDAdded) {
            return;
        }
        addVariable(new Variable(new Declaration(Scope.PRIVATE, true, true, false), new Class("long"), "serialVersionUID") {

            @Override public String getInitialization() {
                try {
                    ByteArrayOutputStream bout = new ByteArrayOutputStream();
                    DataOutputStream dout = new DataOutputStream(bout);

                    dout.writeUTF(getName());
                    generateDeclarationUID(dout);
                    generateInterfaceUID(dout);
                    generateFieldsUID(dout);
                    generateStaticAreaUID(dout);
                    generateConstructorUID(dout);
                    generateMethodsUID(dout);

                    dout.flush();

                    MessageDigest md = MessageDigest.getInstance("SHA");
                    byte[] hashBytes = md.digest(bout.toByteArray());
                    long hash = 0;
                    for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
                        hash = (hash << 8) | (hashBytes[i] & 0xFF);
                    }

                    return Long.toString(hash);
                } catch (IOException ex) {
                    throw new RuntimeException();
                } catch (NoSuchAlgorithmException ex) {
                    throw new SecurityException(ex.getMessage());
                }
            }
        });
    }

    /**
     * Add a <code>Variable</code> to the <code>ClassBody</code>.
     * 
     * @param variable
     *            <code>Variable</code> to be added.
     */
    public void addVariable(Variable variable) {
        variables.add(variable);
    }

    @Override public Object clone() throws CloneNotSupportedException {
        ClassBody clone = new ClassBody();
        cloneContents(clone);

        for (Iterator<Class> iter = _implements.iterator(); iter.hasNext();) {
            clone.addImplements((Class)iter.next().clone());
        }

        return clone;
    }

    /**
     * Retrieve the constructors declared by the <code>ClassBody</code>.
     * 
     * @return Unmodifiable <code>Collection</code> of constructors.
     */
    public Collection<Method> getConstructors() {
        return Collections.unmodifiableCollection(constructors);
    }

    /**
     * Retrieve the <code>Class</code> being extended.<br>
     * 
     * @return If the body extends another class or interface it will be returned - if no super class is set <code>null</code> is returned.
     */
    public Class getExtendsClass() {
        if (extendsClasses.isEmpty()) {
            return null;
        }
        return extendsClasses.get(0);
    }

    /**
     * Retrieve the <code>Classes</code> implemented by the <code>ClassBody</code>
     * 
     * @return <code>Set</code> of <code>Classes</code> be implemented by the <code>ClassBody</code> (<code>Set&lt;Class&gt;</code>)
     */
    public Set<Class> getImplements() {
        return Collections.unmodifiableSet(_implements);
    }

    /**
     * Retrieve all the <code>Methods</code> of the <code>ClassBody</code>.
     * 
     * @return <code>List</code> of declared <code>Methods</code>.
     */
    public List<Method> getMethods() {
        List<Method> methodsList = new ArrayList<Method>();
        for (Iterator<Set<Method>> iter = methods.values().iterator(); iter.hasNext();) {
            Set<Method> set = iter.next();
            methodsList.addAll(set);
        }

        return methodsList;
    }

    /**
     * Retrieve an <code>ImportList</code> containing all the <code>Imports</code> necessary for the <code>ClassBody</code> to function.<br>
     * The <code>ImportList</code> contains both the <code>Imports</code> needed by the <code>ClassBody</code><br>
     * and the <code>Imports</code> needed by the contained <code>Methods</code> and inner classes.
     * 
     * @return <code>ImportList</code> of needed <code>Imports</code>.
     */
    @Override public ImportsList getNeededImports() {
        addNeededImportsFromMethods(constructors);

        addNeededImportsFromVariables();

        addNeededImportsFromStaticAreas();

        return super.getNeededImports();
    }

    /**
     * Retrieve the <code>StaticAreas</code> of the <code>ClassBody</code>.
     * 
     * @return <code>Collection</code> of <code>StaticAreas</code> (<code>Collection&lt;StaticArea&gt;</code>)
     */
    public Collection<StaticArea> getStaticAreas() {
        return Collections.unmodifiableCollection(staticAreas);
    }

    /**
     * Retrieve the <code>Variables</code> contained by the <code>ClassBody</code>.
     * 
     * @return <code>Collection</code> of <code>Variables</code> (<code>Collection&lt;Variable&gt;</code>)
     */
    public Collection<Variable> getVariables() {
        return Collections.unmodifiableCollection(variables);
    }

    /**
     * Set or update the <code>super</code> class of the body.<br>
     * The specified <code>Class</code> will be append to the body header:
     * 
     * <pre>
     *      public class Test extends &lt;b&gt;SuperClass/&lt;b&gt;
     * </pre>
     * 
     * @param extendsClass
     *            The <code>Class</code> to extend - or <code>null</code> to remove any current <code>super</code> class.
     */
    public void setExtendsClass(Class extendsClass) {
        this.extendsClasses.clear();
        this.extendsClasses.add(extendsClass);
    }

    @Override public void setName(String name) {
        super.setName(name);

        for (Iterator<Method> iter = constructors.iterator(); iter.hasNext();) {
            iter.next().setName(name);
        }
    }

    /**
     * Adds a <code>StaticArea</code> to the <code>ClassBody</code>.<br>
     * The <code>StaticArea</code> will be inserted before any variables.
     * 
     * @param staticArea
     *            The <code>StaticArea</code> to set.
     */
    public void addStaticArea(StaticArea staticArea) {
        this.staticAreas.add(staticArea);
    }

    /**
     * Clones the attributes of <code>this</code> <code>ClassBody</code> to the supplied <code>ClassBody</code>.
     * 
     * @param clone
     *            The <code>ClassBody</code> to be populated with the attributes of <code>this</code> <code>ClassBody</code>.
     * @throws CloneNotSupportedException
     *             Thrown if the clone operation fails.
     */
    protected void cloneContents(ClassBody clone) throws CloneNotSupportedException {
        cloneDefaultAttributes(clone);

        for (Iterator<Method> iter = constructors.iterator(); iter.hasNext();) {
            clone.addConstructor((Method)iter.next().clone());
        }

        for (Iterator<Variable> iter = variables.iterator(); iter.hasNext();) {
            Variable entry = iter.next();
            clone.variables.add((Variable)entry.clone());
        }
    }

    private void addNeededImportsFromStaticAreas() {
        for (Iterator<StaticArea> iter = staticAreas.iterator(); iter.hasNext();) {
            importsUsedByBody.addAll(iter.next().getNeededImports());
        }
    }

    private void addNeededImportsFromVariables() {
        for (Iterator<Variable> iter = variables.iterator(); iter.hasNext();) {
            importsUsedByBody.addAll(iter.next().getNeededImports());
        }
    }

    private int calculateModifier(Declaration declaration) {
        int classMods = 0;

        Scope scope = declaration.getScope();
        if (Scope.PUBLIC.equals(scope)) {
            classMods = Modifier.PUBLIC;
        } else if (Scope.PROTECTED.equals(scope)) {
            classMods = Modifier.PROTECTED;
        } else if (Scope.PRIVATE.equals(scope)) {
            classMods = Modifier.PRIVATE;
        }

        classMods &= (declaration.isAbstract() ? Modifier.ABSTRACT : classMods);
        classMods &= (declaration.isStatic() ? Modifier.STATIC : classMods);
        classMods &= (declaration.isFinal() ? Modifier.FINAL : classMods);

        return classMods;
    }

    private List<Method> extractAllMethods(Map<String, Set<Method>> methods) {
        List<Method> allMethods = new ArrayList<Method>();

        for (Set<Method> tmpMethods : methods.values()) {
            allMethods.addAll(tmpMethods);
        }

        return allMethods;
    }

    private void generateConstructorUID(DataOutputStream dout) throws IOException {
        MemberSignature[] consSigs = new MemberSignature[constructors.size()];
        Iterator<Method> iterator = constructors.iterator();
        for (int i = 0; i < consSigs.length; i++) {
            consSigs[i] = new MemberSignature(iterator.next());
        }
        Arrays.sort(consSigs, new Comparator<MemberSignature>() {

            public int compare(MemberSignature o1, MemberSignature o2) {
                String sig1 = o1.signature;
                String sig2 = o2.signature;
                return sig1.compareTo(sig2);
            }
        });
        for (int i = 0; i < consSigs.length; i++) {
            MemberSignature sig = consSigs[i];
            int mods = calculateModifier(sig.member.getDeclaration());
            if ((mods & Modifier.PRIVATE) == 0) {
                dout.writeUTF("<init>");
                dout.writeInt(mods);
                dout.writeUTF(sig.signature.replace('/', '.'));
            }
        }
    }

    private void generateDeclarationUID(DataOutputStream dout) throws IOException {
        int classMods = calculateModifier(getDeclaration()) & (Modifier.PUBLIC | Modifier.FINAL | Modifier.INTERFACE | Modifier.ABSTRACT);
        dout.writeInt(classMods);
    }

    private void generateFieldsUID(DataOutputStream dout) throws IOException {
        MemberSignature[] fieldSigs = new MemberSignature[variables.size()];
        Iterator<Variable> iterator = variables.iterator();
        for (int i = 0; i < fieldSigs.length; i++) {
            fieldSigs[i] = new MemberSignature(iterator.next());
        }
        Arrays.sort(fieldSigs, new Comparator<MemberSignature>() {

            public int compare(MemberSignature o1, MemberSignature o2) {
                String name1 = o1.name;
                String name2 = o2.name;
                return name1.compareTo(name2);
            }
        });
        for (int i = 0; i < fieldSigs.length; i++) {
            MemberSignature sig = fieldSigs[i];
            int mods = calculateModifier(sig.member.getDeclaration());
            if (((mods & Modifier.PRIVATE) == 0) || ((mods & (Modifier.STATIC | Modifier.TRANSIENT)) == 0)) {
                dout.writeUTF(sig.name);
                dout.writeInt(mods);
                dout.writeUTF(sig.signature);
            }
        }
    }

    private void generateInterfaceUID(DataOutputStream dout) throws IOException {
        Set<Class> interfaces = new TreeSet<Class>(getImplements());

        for (Class _interface : interfaces) {
            dout.writeUTF(_interface.getPackageName());
        }
    }

    private void generateMethodsUID(DataOutputStream dout) throws IOException {
        List<Method> allMethods = extractAllMethods(methods);
        MemberSignature[] methSigs = new MemberSignature[allMethods.size()];
        Iterator<Method> iterator = allMethods.iterator();
        for (int i = 0; i < methSigs.length; i++) {
            methSigs[i] = new MemberSignature(iterator.next());
        }
        Arrays.sort(methSigs, new Comparator<MemberSignature>() {

            public int compare(MemberSignature ms1, MemberSignature ms2) {
                int comp = ms1.name.compareTo(ms2.name);
                if (comp == 0) {
                    comp = ms1.signature.compareTo(ms2.signature);
                }
                return comp;
            }
        });
        for (int i = 0; i < methSigs.length; i++) {
            MemberSignature sig = methSigs[i];
            int mods = calculateModifier(sig.member.getDeclaration());
            if ((mods & Modifier.PRIVATE) == 0) {
                dout.writeUTF(sig.name);
                dout.writeInt(mods);
                dout.writeUTF(sig.signature.replace('/', '.'));
            }
        }
    }

    private void generateStaticAreaUID(DataOutputStream dout) throws IOException {
        if (!staticAreas.isEmpty() || hasStaticArea(variables)) {
            dout.writeUTF("<clinit>");
            dout.writeInt(Modifier.STATIC);
            dout.writeUTF("()V");
        }
    }

    private boolean hasStaticArea(Set<Variable> variables) {
        for (Variable variable : variables) {
            if (variable.getStaticArea() != null) {
                return true;
            }
        }
        return false;
    }

    private final class ClassNameComparator implements Comparator<Class> {

        public int compare(Class o1, Class o2) {
            return o1.getClassName().compareTo(o2.getClassName());
        }
    }

    private static class MemberSignature {

        private static String getClassSignature(Class cl) {
            StringBuilder sbuf = new StringBuilder();

            for (int i = 0; i < cl.getArrayDimensions(); i++) {
                sbuf.append('[');
            }

            String _class = cl.getClassName();
            if (cl.isPrimitive()) {
                if ("long".equals(_class)) {
                    sbuf.append('J');
                }
                if ("int".equals(_class)) {
                    sbuf.append('I');
                }
                if ("char".equals(_class)) {
                    sbuf.append('C');
                }
                if ("boolean".equals(_class)) {
                    sbuf.append('Z');
                }
                if ("double".equals(_class)) {
                    sbuf.append('D');
                }
                if ("float".equals(_class)) {
                    sbuf.append('F');
                }
                if ("byte".equals(_class)) {
                    sbuf.append('B');
                }
                if ("short".equals(_class)) {
                    sbuf.append('S');
                }
            } else if (cl.isVoid()) {
                sbuf.append('V');
            } else {
                sbuf.append('L' + cl.getPackageName().replace('.', '/') + ';');
            }
            return sbuf.toString();
        }

        private static String getMethodSignature(List<Argument> arguments, Class retType) {
            StringBuilder sbuf = new StringBuilder();
            sbuf.append('(');
            for (Argument argument : arguments) {
                sbuf.append(getClassSignature(argument.getType()));
            }
            sbuf.append(')');
            sbuf.append(getClassSignature(retType));
            return sbuf.toString();
        }

        /* pp */final DeclarableObject member;

        /* pp */final String name;

        /* pp */final String signature;

        /* pp */MemberSignature(Method method) {
            member = method;
            name = method.getName();
            signature = getMethodSignature(method.getArguments(), method.getReturnType());
        }

        /* pp */MemberSignature(Variable field) {
            member = field;
            name = field.getName();
            signature = getClassSignature(field.getType());
        }
    }
}